/*
 * @(#)VisibleMemberMap.java 1.37 04/06/27
 * 
 * Copyright 2004 Sun Microsystems, Inc. All rights reserved. SUN
 * PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
 */

package com.sun.tools.doclets.internal.toolkit.util;

import com.sun.javadoc.*;
import com.sun.tools.doclets.internal.toolkit.*;
import java.util.*;

/**
 * A data structure that encapsulates the visible members of a particular type
 * for a given class tree. To use this data structor, you must specify the type
 * of member you are interested in (nested class, field, constructor or method)
 * and the leaf of the class tree. The data structure will map all visible
 * members in the leaf and classes above the leaf in the tree.
 * 
 * This code is not part of an API. It is implementation that is subject to
 * change. Do not use it as an API
 * 
 * @author Atul M Dambalkar
 * @author Jamie Ho (rewrite)
 */
public class VisibleMemberMap {
	
	private boolean noVisibleMembers = true;
	
	public static final int INNERCLASSES = 0;
	public static final int ENUM_CONSTANTS = 1;
	public static final int FIELDS = 2;
	public static final int CONSTRUCTORS = 3;
	public static final int METHODS = 4;
	public static final int ANNOTATION_TYPE_MEMBER_OPTIONAL = 5;
	public static final int ANNOTATION_TYPE_MEMBER_REQUIRED = 6;
	
	/**
	 * The total number of member types is {@value}.
	 */
	public static final int NUM_MEMBER_TYPES = 7;
	
	public static final String STARTLEVEL = "start";
	
	/**
	 * List of ClassDoc objects for which ClassMembers objects are built.
	 */
	private final List<ClassDoc> visibleClasses = new ArrayList<ClassDoc>();
	
	/**
	 * Map for each member name on to a map which contains members with same
	 * name-signature. The mapped map will contain mapping for each MemberDoc
	 * onto it's respecive level string.
	 */
	private final Map memberNameMap = new HashMap();
	
	/**
	 * Map of class and it's ClassMembers object.
	 */
	private final Map<ClassDoc, ClassMembers> classMap = new HashMap<ClassDoc, ClassMembers>();
	
	/**
	 * Type whose visible members are requested. This is the leaf of the class
	 * tree being mapped.
	 */
	private final ClassDoc classdoc;
	
	/**
	 * Member kind: InnerClasses/Fields/Methods?
	 */
	private final int kind;
	
	/**
	 * Deprected members should be excluded or not?
	 */
	private final boolean nodepr;
	
	/**
	 * Construct a VisibleMemberMap of the given type for the given class. If
	 * nodepr is true, exclude the deprecated members from the map.
	 * 
	 * @param classdoc the class whose members are being mapped.
	 * @param kind the kind of member that is being mapped.
	 * @param nodepr if true, exclude the deprecated members from the map.
	 */
	public VisibleMemberMap(ClassDoc classdoc, int kind, boolean nodepr) {
		this.classdoc = classdoc;
		this.nodepr = nodepr;
		this.kind = kind;
		new ClassMembers(classdoc, STARTLEVEL).build();
	}
	
	/**
	 * Return the list of visible classes in this map.
	 * 
	 * @return the list of visible classes in this map.
	 */
	public List getVisibleClassesList() {
		sort(visibleClasses);
		return visibleClasses;
	}
	
	/**
	 * Return the package private members inherited by the class. Only return if
	 * parent is package private and not documented.
	 * 
	 * @param configuation the current configuration of the doclet.
	 * @return the package private members inherited by the class.
	 */
	private List getInheritedPackagePrivateMethods(Configuration configuration) {
		List results = new ArrayList();
		for (Iterator iter = visibleClasses.iterator(); iter.hasNext();) {
			ClassDoc currentClass = (ClassDoc) iter.next();
			if (currentClass != classdoc && currentClass.isPackagePrivate()
					&& !Util.isLinkable(currentClass, configuration)) {
				// Document these members in the child class because
				// the parent is inaccessible.
				results.addAll(getMembersFor(currentClass));
			}
		}
		return results;
	}
	
	/**
	 * Return the visible members of the class being mapped. Also append at the
	 * end of the list members that are inherited by inaccessible parents. We
	 * document these members in the child because the parent is not documented.
	 * 
	 * @param configuration the current configuration of the doclet.
	 */
	public List getLeafClassMembers(Configuration configuration) {
		List result = getMembersFor(classdoc);
		result.addAll(getInheritedPackagePrivateMethods(configuration));
		return result;
	}
	
	/**
	 * Retrn the list of members for the given class.
	 * 
	 * @param cd the class to retrieve the list of visible members for.
	 * 
	 * @return the list of members for the given class.
	 */
	public List getMembersFor(ClassDoc cd) {
		ClassMembers clmembers = (ClassMembers) (classMap.get(cd));
		if (clmembers == null) {
			return new ArrayList();
		}
		return clmembers.getMembers();
	}
	
	/**
	 * Sort the given mixed list of classes and interfaces to a list of classes
	 * followed by interfaces traversed. Don't sort alphabetically.
	 */
	private void sort(List list) {
		List classes = new ArrayList();
		List interfaces = new ArrayList();
		for (int i = 0; i < list.size(); i++) {
			ClassDoc cd = (ClassDoc) list.get(i);
			if (cd.isClass()) {
				classes.add(cd);
			} else {
				interfaces.add(cd);
			}
		}
		list.clear();
		list.addAll(classes);
		list.addAll(interfaces);
	}
	
	private void fillMemberLevelMap(List list, String level) {
		for (int i = 0; i < list.size(); i++) {
			Object key = getMemberKey((ProgramElementDoc) list.get(i));
			Map memberLevelMap = (Map) memberNameMap.get(key);
			if (memberLevelMap == null) {
				memberLevelMap = new HashMap();
				memberNameMap.put(key, memberLevelMap);
			}
			memberLevelMap.put(list.get(i), level);
		}
	}
	
	/**
	 * Represents a class member. We should be able to just use a
	 * ProgramElementDoc instead of this class, but that doesn't take type
	 * variables in consideration when comparing.
	 */
	private class ClassMember {
		private Set members;
		
		public ClassMember(ProgramElementDoc programElementDoc) {
			members = new HashSet();
			members.add(programElementDoc);
		}
		
		public void addMember(ProgramElementDoc programElementDoc) {
			members.add(programElementDoc);
		}
		
		public boolean isEqual(MethodDoc member) {
			for (Iterator iter = members.iterator(); iter.hasNext();) {
				MethodDoc member2 = (MethodDoc) iter.next();
				if (Util.executableMembersEqual(member, member2)) {
					members.add(member);
					return true;
				}
			}
			return false;
		}
	}
	
	/**
	 * A data structure that represents the class members for a visible class.
	 */
	private class ClassMembers {
		
		/**
		 * The mapping class, whose inherited members are put in the
		 * {@link #members} list.
		 */
		private ClassDoc mappingClass;
		
		/**
		 * List of inherited members from the mapping class.
		 */
		private List members = new ArrayList();
		
		/**
		 * Level/Depth of inheritance.
		 */
		private String level;
		
		/**
		 * Return list of inherited members from mapping class.
		 * 
		 * @return List Inherited members.
		 */
		public List getMembers() {
			return members;
		}
		
		private ClassMembers(ClassDoc mappingClass, String level) {
			this.mappingClass = mappingClass;
			this.level = level;
			if ((!classMap.containsKey(mappingClass))
					|| level.startsWith(((ClassMembers) classMap.get(mappingClass)).level)) {
				classMap.put(mappingClass, this);
				visibleClasses.add(mappingClass);
			}
			
		}
		
		private void build() {
			if (kind == CONSTRUCTORS) {
				addMembers(mappingClass);
			} else {
				mapClass();
			}
		}
		
		private void mapClass() {
			addMembers(mappingClass);
			ClassDoc[] interfaces = mappingClass.interfaces();
			for (int i = 0; i < interfaces.length; i++) {
				String locallevel = level + 1;
				ClassMembers cm = new ClassMembers(interfaces[i], locallevel);
				cm.mapClass();
			}
			if (mappingClass.isClass()) {
				ClassDoc superclass = mappingClass.superclass();
				if (!(superclass == null || mappingClass.equals(superclass))) {
					ClassMembers cm = new ClassMembers(superclass, level + "c");
					cm.mapClass();
				}
			}
		}
		
		/**
		 * Get all the valid members from the mapping class. Get the list of
		 * members for the class to be included into(ctii), also get the level
		 * string for ctii. If mapping class member is not already in the
		 * inherited member list and if it is visible in the ctii and not
		 * overridden, put such a member in the inherited member list. Adjust
		 * member-level-map, class-map.
		 */
		private void addMembers(ClassDoc fromClass) {
			List cdmembers = getClassMembers(fromClass, true);
			List incllist = new ArrayList();
			for (int i = 0; i < cdmembers.size(); i++) {
				ProgramElementDoc pgmelem = (ProgramElementDoc) (cdmembers.get(i));
				if (!found(members, pgmelem) && memberIsVisible(pgmelem)
						&& !isOverridden(pgmelem, level)) {
					incllist.add(pgmelem);
				}
			}
			if (incllist.size() > 0) {
				noVisibleMembers = false;
			}
			members.addAll(incllist);
			fillMemberLevelMap(getClassMembers(fromClass, false), level);
		}
		
		/**
		 * Is given doc item visible in given classdoc in terms fo inheritance?
		 * The given doc item is visible in the given classdoc if it is public
		 * or protected and if it is package-private if it's containing class is
		 * in the same package as the given classdoc.
		 */
		private boolean memberIsVisible(ProgramElementDoc pgmdoc) {
			if (pgmdoc.containingClass().equals(classdoc)) {
				// Member is in class that we are finding visible members for.
				// Of course it is visible.
				return true;
			} else if (pgmdoc.isPrivate()) {
				// Member is in super class or implemented interface.
				// Private, so not inherited.
				return false;
			} else if (pgmdoc.isPackagePrivate()) {
				// Member is package private. Only return true if its class is
				// in
				// same package.
				return pgmdoc.containingClass().containingPackage().equals(
						classdoc.containingPackage());
			} else {
				// Public members are always inherited.
				return true;
			}
		}
		
		/**
		 * Return all available class members.
		 */
		private List getClassMembers(ClassDoc cd, boolean filter) {
			if (cd.isEnum() && kind == CONSTRUCTORS) {
				// If any of these rules are hit, return empty array because
				// we don't document these members ever.
				return Arrays.asList(new ProgramElementDoc[] {});
			}
			ProgramElementDoc[] members = null;
			switch (kind) {
				case ANNOTATION_TYPE_MEMBER_OPTIONAL:
					members = cd.isAnnotationType()
							? filter((AnnotationTypeDoc) cd, false)
							: new AnnotationTypeElementDoc[] {};
					break;
				case ANNOTATION_TYPE_MEMBER_REQUIRED:
					members = cd.isAnnotationType()
							? filter((AnnotationTypeDoc) cd, true)
							: new AnnotationTypeElementDoc[] {};
					break;
				case INNERCLASSES:
					members = cd.innerClasses(filter);
					break;
				case ENUM_CONSTANTS:
					members = cd.enumConstants();
					break;
				case FIELDS:
					members = cd.fields(filter);
					break;
				case CONSTRUCTORS:
					members = cd.constructors();
					break;
				case METHODS:
					members = cd.methods(filter);
					break;
				default:
					members = new ProgramElementDoc[0];
			}
			if (nodepr) {
				return Util.excludeDeprecatedMembersAsList(members);
			}
			return Arrays.asList(members);
		}
		
		/**
		 * Filter the annotation type members and return either the required
		 * members or the optional members, depending on the value of the
		 * required parameter.
		 * 
		 * @param doc The annotation type to process.
		 * @param required
		 * @return the annotation type members and return either the required
		 *         members or the optional members, depending on the value of
		 *         the required parameter.
		 */
		private AnnotationTypeElementDoc[] filter(AnnotationTypeDoc doc,
				boolean required) {
			AnnotationTypeElementDoc[] members = ((AnnotationTypeDoc) doc).elements();
			List targetMembers = new ArrayList();
			for (int i = 0; i < members.length; i++) {
				if ((required && members[i].defaultValue() == null)
						|| ((!required) && members[i].defaultValue() != null)) {
					targetMembers.add(members[i]);
				}
			}
			return (AnnotationTypeElementDoc[]) targetMembers.toArray(new AnnotationTypeElementDoc[] {});
		}
		
		private boolean found(List list, ProgramElementDoc elem) {
			for (int i = 0; i < list.size(); i++) {
				ProgramElementDoc pgmelem = (ProgramElementDoc) list.get(i);
				if (Util.matches(pgmelem, elem)) {
					return true;
				}
			}
			return false;
		}
		
		/**
		 * Is member overridden? The member is overridden if it is found in the
		 * same level hierarchy e.g. member at level "11" overrides member at
		 * level "111".
		 */
		private boolean isOverridden(ProgramElementDoc pgmdoc, String level) {
			Map memberLevelMap = (Map) memberNameMap.get(getMemberKey(pgmdoc));
			if (memberLevelMap == null)
				return false;
			String mappedlevel = null;
			Iterator iterator = memberLevelMap.values().iterator();
			while (iterator.hasNext()) {
				mappedlevel = (String) (iterator.next());
				if (mappedlevel.equals(STARTLEVEL)
						|| (level.startsWith(mappedlevel) && !level.equals(mappedlevel))) {
					return true;
				}
			}
			return false;
		}
	}
	
	/**
	 * Return true if this map has no visible members.
	 * 
	 * @return true if this map has no visible members.
	 */
	public boolean noVisibleMembers() {
		return noVisibleMembers;
	}
	
	private ClassMember getClassMember(MethodDoc member) {
		for (Iterator iter = memberNameMap.keySet().iterator(); iter.hasNext();) {
			Object key = iter.next();
			if (key instanceof String) {
				continue;
			} else if (((ClassMember) key).isEqual(member)) {
				return (ClassMember) key;
			}
		}
		return new ClassMember(member);
	}
	
	/**
	 * Return the key to the member map for the given member.
	 */
	private Object getMemberKey(ProgramElementDoc doc) {
		if (doc.isConstructor()) {
			return doc.name() + ((ExecutableMemberDoc) doc).signature();
		} else if (doc.isMethod()) {
			return getClassMember((MethodDoc) doc);
		} else if (doc.isField() || doc.isEnumConstant()
				|| doc.isAnnotationTypeElement()) {
			return doc.name();
		} else { // it's a class or interface
			String classOrIntName = doc.name();
			// Strip off the containing class name because we only want the
			// member name.
			classOrIntName = classOrIntName.indexOf('.') != 0
					? classOrIntName.substring(classOrIntName.lastIndexOf('.'),
							classOrIntName.length())
					: classOrIntName;
			return "clint" + classOrIntName;
		}
	}
}
